#!/usr/bin/python
import json
import select
import sys
from datetime import datetime, time
import utils

from Config import ConfigModule
from CursesDisplay import curses, CursesDisplay, MessageDialog, Menu
from HttpClient import HttpManager
from DataRequester import RTDataRequester
from Share import ShareGroup, IndexGroup

class SafeViewController():
    enabled = False
    grace_time = 5
    unsafe_modes = ["name", "code"]
    last_safe_view_update = 0
    remaining_time = 0
    @classmethod
    def config(cls, options):
        if options and isinstance(options, dict):
            cls.enabled = options.get('enabled', cls.enabled)
            cls.grace_time = options.get('grace_time', cls.grace_time)
            cls.unsafe_modes = options.get('unsafe_modes', cls.unsafe_modes)
        if cls.enabled:
            cls.last_safe_view_update = utils.get_time_tick()
    @classmethod
    def have_to_switch(cls, current_mode):
        if not cls.is_safe(current_mode):
            cls.remaining_time = cls.grace_time - utils.get_time_tick() \
                + cls.last_safe_view_update
            if cls.remaining_time <= 0:
                cls.reset()
                return True
            return False
        cls.reset()
        return False
    @classmethod
    def get_remaining_time(cls):
        return 0 if cls.remaining_time < 0 else cls.remaining_time
    @classmethod
    def is_safe(cls, current_mode):
        return not cls.enabled or current_mode not in cls.unsafe_modes
    @classmethod
    def reset(cls):
        if cls.enabled:
            cls.last_safe_view_update = utils.get_time_tick()
            cls.remaining_time = 0

class IdleLogoutController():
    enabled = False
    idle_time = 0
    timeout = 300
    remaining_time = 0
    @classmethod
    def config(cls, options):
        if options and isinstance(options, dict):
            cls.enabled = options.get('enabled', cls.enabled)
            cls.timeout = options.get('timeout', cls.timeout)
        if cls.enabled:
            cls.idle_time = utils.get_time_tick()
    @classmethod
    def have_to_logout(cls):
        if cls.enabled:
            cls.remaining_time = cls.timeout - utils.get_time_tick() \
                + cls.idle_time
            if cls.remaining_time <= 0:
                cls.idle_time = utils.get_time_tick()
                return True
        return False
    @classmethod
    def get_remaining_time(cls):
        return 0 if cls.remaining_time < 0 else cls.remaining_time
    @classmethod
    def reset(cls):
        if cls.enabled:
            cls.idle_time = utils.get_time_tick()
            cls.remaining_time = 0

class ViewMode():
    ''' standard view-mode '''
    @staticmethod
    def create_by_name(mode_name):
        return ViewMode(mode_name)
    def __init__(self, name):
        #self.isSafe = False
        self.mode_name = name
    def get_line(self, share, summary=0.0):
        ret = []
        (
            code, name, _cur, _low, _high,
            _rate, _last, _cost, _count,
            _rev, _diff
        ) = (
            share.id, share.name, share.cur, share.low, share.high,
            share.rate, share.last, share.cost_ps, share.amount,
            share.revenue, share.diff
        )
        hz_count = utils.count_hz(share.name)
        dbl_max_hc = 18
        if self.mode_name == 'mount':
            linefmt = "{0:<%d}  {1:>8.0f}  {2:>8.0f}  {3:>8.0f}  {4:>7.2f}%%{5:<s}" \
                      % dbl_max_hc
        elif self.mode_name == 'name':
            ## why not 2 spaces before variable 5?
            ##   want a nice reversed display when moving select bar
            ##   if adding 2 spaces, entry without tags would get a
            ##   2 spaces longer reversed color bar.
            linefmt = "{0:<%d}  {1:>8.2f}  {2:>8.2f}  {3:>8.2f}  {4:>7.2f}%%{5:<s}" \
                      % (dbl_max_hc-hz_count)
        else:
            linefmt = "{0:<%d}  {1:>8.2f}  {2:>8.2f}  {3:>8.2f}  {4:>7.2f}%%{5:<s}" \
                      % (dbl_max_hc)

        ## display a pointless value in mount mode
        disp_cur = _cur
        if 'mount' == self.mode_name:
            disp_cur *= 100
            _low *= 100
            _high *= 100
        _tags = ''
        if share.tags:
            _tags = '  '+ ','.join(share.tags)

        ret.append(linefmt.format(self.get_disp_name(share), disp_cur, _low, _high, _rate, _tags))

        if _count > 0:
            pos_fmt = "{0:>%ds}  {1:>8.2f}  {2:>8.0f}  {3:>8.0f} {4:>9.2f}" % dbl_max_hc
            _worth = _cur * _count / 1000
            if 'mount' == self.mode_name:
                pos_fmt = "{0:>%ds}  {1:>8.0f}  {2:>8.0f}  {3:>8.0f} {4:>9.0f}" % dbl_max_hc
                _cost *= 100
            _paid = 0.001 if share.paid == 0 else share.paid
            _profit_rate = _worth * 100 * 1000 / _paid - 100
            _profit_rate = 999.9 if _profit_rate > 999.9 else _profit_rate
            if summary != 0:
                _value = "{1:>4.1f}%,{0:>0.1f} {2:>4.1f}%".format(
                    _worth,
                    _cur * _count * 100 / summary,
                    _profit_rate
                )
            else:
                _value = "{0:>0.1f}".format(_worth)
            ret.append(pos_fmt.format(_value, _cost, _count, _rev, _diff))
        return ret

    def get_disp_name(self, share):
        s_name = ''
        if 'name' == self.mode_name:
            s_name = share.name
        elif 'code' == self.mode_name:
            s_name = share.id
        elif 'abbr' == self.mode_name:
            s_name = utils.get_abbr(share.name)
        elif 'mount' == self.mode_name:
            s_name = '/dev/vda/{}'.format(utils.get_abbr(share.name))
        if share.amount > 0:
            if share.cur >= share.cost_ps:
                s_name += '+'
            else:
                s_name += '-'
        return s_name

class ViewModeManager():
    display_modes = {k:None for k in ['code', 'name', 'abbr', 'mount']}
    current = ''
    safe_view_mode = 'mount'
    @classmethod
    def config(cls, init_mode):
        cls.current = init_mode
    @classmethod
    def switch_view_mode(cls):
        mode_names = list(cls.display_modes.keys())
        cls.current = mode_names[
            (mode_names.index(cls.current) + 1) % len(mode_names)
        ]
    @classmethod
    def get_heads(cls):
        col_names = ['Name', 'Current', 'Bottom', 'Top', 'Percent']
        if 'name' == cls.current:
            col_names[0] = 'Name'
        elif 'code' == cls.current:
            col_names[0] = 'Code'
        elif 'abbr' == cls.current:
            col_names[0] = 'Abbr.'
        elif 'mount' == cls.current:
            col_names = ['Filesystem',
                         '1K-blocks',
                         'Used',
                         'Available',
                         'Use']
        return '%-18s %9s %9s %9s %8s%%' % tuple(col_names)

    @classmethod
    def get_line(cls, share, summary=0.0):
        if not cls.display_modes[cls.current]:
            cls.display_modes[cls.current] = \
                ViewMode.create_by_name(cls.current)
        _vm = cls.display_modes[cls.current]
        return _vm.get_line(share, summary)

class SharesView():
    messages = []
    watch_idx = 0
    current_share_id = ''
    update_mode = 'ACTIVE'
    standby_time = 0
    order_by = 'price'
    view_mode = ''
    safe_view_mode = 'mount'
    stock_list = []
    current_share_idx = -1
    line_no = 0

    def __init__(self):
        self.cfg = self.load_config_file()
        if not self.cfg:
            sys.exit(9)
        self.load_config()
        self.current_tick = datetime.now()

        self.idx_group = IndexGroup()
        ## try to update before dp initializing
        ##   so config or connection error could
        ##   be detected outside curses mode
        self.update()

        self.dp = CursesDisplay({
            'colors': {
                'default': 256
            }
        })

    def map_key(self):
        _key = self.dp.getInput()
        if ord('?') == _key:
            self.show_usage()
        elif ord('v') == _key:
            ViewModeManager.switch_view_mode()
        elif curses.KEY_F1 == _key:
            self.add_message("F1")
        elif curses.KEY_F5 == _key:
            self.reload()
        elif ord('x') == _key:
            return 1
        elif curses.KEY_UP == _key:
            self.select_share('prev')
        elif curses.KEY_DOWN == _key:
            self.select_share('next')
        elif curses.KEY_LEFT == _key:
            self.switch_watch_group('prev')
        elif curses.KEY_RIGHT == _key:
            self.switch_watch_group('next')

        if ord('g') == _key:
            self.show_groups()
        if ord('h') == _key:
            if SafeViewController.enabled:
                SafeViewController.enabled = False
                self.add_message('HOME mode enabled!')
            else:
                SafeViewController.enabled = True
                SafeViewController.reset()
                self.add_message('HOME mode disabled!')
        elif ord('b') == _key:
            self.switch_watch_group('Banks&Insurence')
        elif ord('d') == _key:
            self.debug_mode()
        elif ord('p') == _key:
            self.switch_watch_group('*positions*')
        elif ord('q') == _key:
            self.switch_watch_group('*quitted*')
        elif ord('c') == _key:
            self.switch_watch_group('Category')
        elif ord('s') == _key:
            self.switch_order_by()
        elif ord('+') == _key:
            self.increase_interval(1)
        elif ord('-') == _key:
            self.increase_interval(-1)
        elif curses.KEY_ESC == _key:
            self.current_share_idx = -1
            self.add_message('ESC')
            self.select_share('')
        else:
            self.add_message('got: ' + str(_key))
        IdleLogoutController.reset()
        return 0

    def reference_group(self, _group):
        _ref = self.cfg.get_value(_group.reference, [])
        if _ref:
            _group.name = '*{}*'.format(_group.name)
            _group.share_ids = [p['id'] for p in _ref]

    def load_config_file(self, view=None):
        config_path = './config.json'
        if len(sys.argv) <= 1:
            #print('use default conf!')
            pass
        else:
            config_path = sys.argv[1]
        try:
            _config = ConfigModule.load(config_path)
        except json.decoder.JSONDecodeError as jde:
            lines = jde.doc.split('\n')
            if view:
                self.add_message('Failed to load {}'.format(config_path))
                for _index in range(jde.lineno-1, jde.lineno-3, -1):
                    self.add_message('{}: {}'.format(_index + 1, lines[_index]))
                self.add_message(jde)
                _config = None
            else:
                print('Failed to load {}'.format(config_path))
                for _index in range(jde.lineno-3, jde.lineno):
                    print('{}: {}'.format(_index + 1, lines[_index]))
                utils.safe_exit(17, jde)
        return _config

    def load_config(self):
        HttpManager.config(
            self.cfg.get_value("common", "proxies", [])
        )
        self.data_source = RTDataRequester.create(
            self.cfg.get_value("common", "data_source", ""),
            HttpManager.get_client()
        )
        if not self.data_source:
            self.add_message("No valid data_source provisioned")
        ViewModeManager.config(
            self.cfg.get_value("display", "mode", 'name')
        )
        #self.view_mode = self.cfg.get_value("display", "mode", 'name')
        self.interval = self.cfg.get_value("display", "interval", 1.0)
        SafeViewController.config(
            self.cfg.get_value("display", "safe_view", {})
        )
        IdleLogoutController.config(
            self.cfg.get_value("common", "idle_logout", {})
        )
        self.time_control = {
            'working_days': self.cfg.get_value("common", "working_days", ''),
            'working_time': self.cfg.get_value("common", "working_time", ''),
        }
        # default auto idle logout time: 5min
        self.idle_logout = self.cfg.get_value("common", "idle_logout", 300)

        self.title = self.cfg.get_value("common", "title", '')
        self.stock_list = self.cfg.get_value('stock', 'watches', [])

        self.watch_list = self.cfg.get_value('watch_list', [])
        if not self.watch_list:
            ## back-compatible for old "stocks" list only config
            self.watch_list = [{
                'name': self.title,
                'stocks': self.stock_list
            }]
        self.share_groups = ShareGroup.load_groups(self.watch_list)
        for _group in self.share_groups:
            if _group.reference:
                #self.add_message("load ref-group: %s" %_group.reference)
                self.reference_group(_group)

        self.positions = self.cfg.get_value('positions', [])
        self.quitted = self.cfg.get_value('quitted', [])

        if self.watch_idx >= len(self.watch_list):
            self.watch_idx = 0
    def reload(self):
        _config = self.load_config_file(True)
        if _config:
            self.current_share_id = ''
            self.cfg = _config
            self.load_config()
            self.add_message('config reloaded, {} groups'.format(
                len(self.share_groups)
            ))
            self.update_mode = 'ACTIVE'

    def run(self):
        try:
            while 1:
                infds, outfds, errfds = select.select(
                    [0,], [], [], self.interval
                )
                if len(infds) > 0:
                    if self.map_key():
                        break
                self.update()
        finally:
            self.destroy()

    def switch_order_by(self):
        order_bies = [
            'price', 'value', 'daily-balance', 'revenue', 'cost', 'tag'
        ]
        self.order_by = order_bies[
            (order_bies.index(self.order_by) + 1) % len(order_bies)
        ]

    def switch_watch_group(self, _dir):
        _last_group = self.watch_idx
        if 'prev' == _dir:
            self.watch_idx = (self.watch_idx-1) % len(self.watch_list)
        elif 'next' == _dir:
            self.watch_idx = (self.watch_idx+1) % len(self.watch_list)
        else:
            _found = [i for i, g in enumerate(self.share_groups)
                      if g.name == _dir]
            if _found:
                self.watch_idx = _found[0]

        if _last_group != self.watch_idx:
            self.current_share_id = ''

        if self.update_mode == 'STANDBY':
            self.update_mode = 'ACTIVE'

    def increase_interval(self, offset):
        _old = self.interval
        self.interval += offset
        if self.interval < 1:
            self.interval = 1
            self.add_message(f'interval: min reached -> {self.interval}')
        elif self.interval > 60:
            self.interval = 60
            self.add_message(f'interval: max reached -> {self.interval}')
        else:
            self.add_message(f'interval: {_old} -> {self.interval}')
    def select_share(self, _dir):
        if 'prev' == _dir:
            self.current_share_id = self.current_group.get_prev()
        elif 'next' == _dir:
            self.current_share_id = self.current_group.get_next()
        else:
            self.current_share_id = ''

    @property
    def current_watch_group(self):
        return self.watch_list[self.watch_idx]
    @property
    def current_group(self):
        return self.share_groups[self.watch_idx]
    @property
    def prev_group(self):
        return self.share_groups[(self.watch_idx-1) % len(self.share_groups)]
    @property
    def next_group(self):
        return self.share_groups[(self.watch_idx+1) % len(self.share_groups)]
    def add_message(self, _msg):
        self.messages.append('{} {}'.format(
            utils.get_localtime().strftime('%T'),
            _msg
        ))
    @staticmethod
    def show_usage():
        MessageDialog(message=[
            'c: Categories',
            'g: Groups',
            'h: Home Mode',
            'p: Positions',
            'q: Emptized Shares',
            's: Switch order-by',
            'v: Switch view mode',
            'F5: Reload Config',
            'x: exit'
        ], title='Usage', height='auto').doModal()
    def show_config(self, section):
        MessageDialog(
            message=utils.dump_json_neat(
                self.cfg.get_value(section, {})
            ),
            title=section, width='auto', height='auto'
        ).doModal()
    def show_proxy_status(self):
        MessageDialog(
            message='\n'.join([
                '{:15} {:5} {}\n  {:>5} {:>5}  {:>5} {:>5}'.format(
                    p.proxy_ip, p.proxy_port, p.status,
                    p.stats.get('success'), p.stats.get('failed'),
                    p.avg_rtt, p.rtt
                ) for p in HttpManager.proxies
            ]),
            title='connections', width='auto', height='auto'
        ).doModal()
    def debug_mode(self):
        import functools
        main_menu = Menu(
            [(
                'config', Menu(
                    [(
                        'common', functools.partial(
                            self.show_config, 'common'
                        )
                    ),
                    (
                        'display', functools.partial(
                            self.show_config, 'display'
                        )
                    ),
                    (
                        'connections', functools.partial(
                            self.show_proxy_status
                        )
                    )],
                    self.dp.scr, 18, 22,
                    exit_label='back'
                ).display
            )], self.dp.scr, 17, 5
        )
        main_menu.display()
    def show_groups(self):
        MessageDialog(
            message=['{0} ({1})'.format(g.name, len(g.share_ids))
                     for g in self.share_groups],
            title='Groups'
        ).doModal()

    def add_line(self, line, highlight=False):
        self.dp.addStrLine(self.line_no, 0, line, highlight)
        self.line_no += 1

    def add_lines(self, lines, highlight=False):
        for line in lines:
            self.add_line(line, highlight)

    def show_share_info(self, share):
        hltd = self.current_share_id == share.id
        self.add_lines(
            ViewModeManager.get_line(
                share, self.current_group.summary['value']
            ), hltd
        )

    def show_idx(self):
        self.add_line('{} | {} | {} {} - {:.0f}ms'.format(
            self.prev_group.name,
            self.current_group.name,
            self.next_group.name,
            self.idx_group.upd_time,
            self.current_group.resp_time * 1000
        ))
        _idx = self.idx_group.shares
        if len(_idx) == 3:
            self.add_line(
                'sh:{cur:6.2f}({rate:3.2f}%) '.format(**_idx[0].__dict__) +
                'sz:{cur:6.2f}({rate:3.2f}%) '.format(**_idx[1].__dict__) +
                'cy:{cur:6.2f}({rate:3.2f}%)'.format(**_idx[2].__dict__)
            )
        else:
            self.add_line('Idx N.A')

    def show_shares(self):
        ## safe view
        if SafeViewController.have_to_switch(ViewModeManager.current):
            ViewModeManager.current = self.safe_view_mode
            self.add_message("back to safe ...")
            self.select_share('')

        self.add_line(ViewModeManager.get_heads())

        if self.order_by == 'price':
            _sort_by = lambda x: x.rate
        elif self.order_by == 'value':
            _sort_by = lambda x: x.value
        elif self.order_by == 'daily-balance':
            _sort_by = lambda x: x.diff
        elif self.order_by == 'revenue':
            _sort_by = lambda x: x.revenue
        elif self.order_by == 'cost':
            _sort_by = lambda x: x.paid
        elif self.order_by == 'tag':
            _sort_by = lambda x: x.tags[0] if x.tags else ''

        self.current_group.shares.sort(key=_sort_by, reverse=True)
        for _share in self.current_group.shares:
            self.show_share_info(_share)

        _group = self.current_group
        if _group.summary:
            self.add_line('%-18s %9.0f %9.0f %9.0f  %8.0f' % (
                'sum (%d/%d/%d)' % (
                    _group.summary['count'],
                    _group.summary['up'],
                    _group.summary['down']
                ),
                _group.summary['value'],
                _group.summary['cost'],
                _group.summary['revenue'],
                _group.summary['diff']
            ))

    def show_footer(self):
        view_mode_text = '{} mode'.format(ViewModeManager.current)
        if not SafeViewController.is_safe(ViewModeManager.current):
            view_mode_text += ' svt:{}s'.format(
                str(SafeViewController.get_remaining_time())
            )

        update_mode_text = self.update_mode
        if self.update_mode == 'STANDBY':
            update_mode_text += ' st:{}s'.format(self.standby_time)
        update_mode_text += ' iot:{}s'.format(
            IdleLogoutController.get_remaining_time()
        )

        self.add_line(
            '[{}] [{} desc] [{}] [{}]'.format(
                view_mode_text,
                self.order_by,
                update_mode_text,
                self.data_source.name if self.data_source else "none"
            )
        )
        ## only show the latest 5
        for _msg in reversed(self.messages[-5:]):
            self.add_line(_msg)

    def show_panel(self):
        if self.current_group and self.current_share_id:
            _lineno = self.current_group.selected_line_no
            if _lineno > -1:
                _contents = self.current_group.selected_share.get_panel_lines()
                _max_width = max([utils.len_w_hz(l) for l in _contents])
                ## panel size
                panel_rows = len(_contents) + 1
                panel_columns = _max_width + 2
                panel_x = 28
                win = self.dp.newwin(
                    panel_rows,
                    panel_columns,
                    4 + _lineno,
                    panel_x
                )
                ##win.border(1)
                win.box()
                win.addstr(
                    0,
                    (_max_width - utils.len_w_hz(_contents[0])) // 2,
                    _contents[0]
                )
                for i in range(1, len(_contents)):
                    win.addstr(i, 1, _contents[i])
                win.refresh()

    def print_messages(self):
        for _msg in self.messages[-5:]:
            print(_msg)

    def reset_dp(self):
        self.line_no = 0
        self.dp.clear()

    def render(self):
        self.reset_dp()

        self.show_idx()
        self.add_line('')
        self.show_shares()
        self.add_line('')
        self.show_footer()
        self.dp.refresh()

        self.show_panel()

    def update(self):
        if IdleLogoutController.have_to_logout():
            self.add_message("Idle timeout, exiting ...")
            utils.safe_exit(0)

        def update_groups(_groups, _result):
            #self.time_to_standby()
            for _group in _groups:
                _group.update({'rt': _result, 'pos': self.positions})

        def load_data_timeout():
            self.add_message('reply timeout')
        def error_during_loading(errmsg):
            self.add_message('data source error: %s' % errmsg)

        if self.update_mode == 'ACTIVE':
            if self.data_source:
                self.data_source.load_data_for_groups(
                    [self.current_group, self.idx_group],
                    {
                        'success': update_groups,
                        'timeout': load_data_timeout,
                        'error': error_during_loading
                    }
                )
            if self.time_to_standby():
                self.update_mode = 'STANDBY'
                self.add_message('switch to STANDBY ...')

        elif self.update_mode == 'STANDBY':
            self.standby_time -= round(
                datetime.now().timestamp() - self.current_tick.timestamp()
            )
            if self.standby_time < 0:
                self.update_mode = 'ACTIVE'
                self.add_message('switch to ACITVE ...')

        self.current_tick = datetime.now()
        if hasattr(self, 'dp'):
            self.render()

    def time_to_standby(self):
        try:
            data_ts = datetime.strptime(
                self.idx_group.upd_time,
                'Y-%m-%d %H:%M:%S'
            )
        except ValueError:
            data_ts = datetime.now()
        local_ts = datetime.now()

        ## data not changed for at least 0.5h
        if local_ts.date() > data_ts.date() or \
           local_ts.hour > data_ts.hour:
            self.add_message('data is out of date')
            self.standby_time = 28800
            return True
        ## market weekday check
        if local_ts.weekday() > 4:
            ## if no data ts, non-working day
            self.add_message('not a trading day')
            self.standby_time = 28800
            return True
        ## market time range check
        ##   morning
        if local_ts.time() < time(9, 25):
            self.add_message('M is not opened')
            self.standby_time = 3600 * (9 - local_ts.hour) + \
                60 * (25 - local_ts.minute) - local_ts.second
            return True
        ##   noon
        if time(11, 32) < local_ts.time() < time(12, 58):
            self.add_message('lunch break')
            self.standby_time = 3600 * (13 - local_ts.hour) + \
                60 * (0 - local_ts.minute) - local_ts.second
            return True
        ##   afternoon
        if local_ts.time() > time(15, 5):
            self.add_message('M was closed')
            self.standby_time = 28800
            return True
        return False

    def destroy(self):
        self.dp.destroy()
        self.add_message('exited from sv')
        self.print_messages()

if __name__ == '__main__':
    config = ConfigModule({
        'my': {}
    })

    sv = SharesView()
    sv.add_message('Hi SV')
    for msg in sv.messages:
        print(msg)
